/*

Copyright (C) SYSTAP, LLC DBA Blazegraph 2006-2016.  All rights reserved.

Contact:
     SYSTAP, LLC DBA Blazegraph
     2501 Calvert ST NW #106
     Washington, DC 20008
     licenses@blazegraph.com

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; version 2 of the License.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

*/
/*
 * Created on May 26, 2009
 */

package com.bigdata.counters.query;

import java.io.File;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.Format;
import java.text.NumberFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.Vector;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.log4j.Logger;

import com.bigdata.counters.History;
import com.bigdata.counters.ICounterSet;
import com.bigdata.counters.PeriodEnum;
import com.bigdata.counters.httpd.CounterSetHTTPD;
import com.bigdata.service.Event;
import com.bigdata.service.IEventReportingService;
import com.bigdata.service.IService;
import com.bigdata.util.CaseInsensitiveStringComparator;
import com.bigdata.util.httpd.NanoHTTPD;

/**
 * The model for a URL used to query an {@link ICounterSelector}.
 * 
 * @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson</a>
 * @version $Id$
 */
public class URLQueryModel {

    private static transient final Logger log = Logger.getLogger(URLQueryModel.class);
    
    /**
     * Name of the URL query parameter specifying the starting path for the page
     * view.
     */
    public static final String PATH = "path";

    /**
     * Depth to be displayed from the given path -or- ZERO (0) to display
     * all levels.
     */
    public static final String DEPTH = "depth";
    
    /**
     * @see BLZG-1318
     */
    public static final String DEFAULT_DEPTH = "0";
    
    /**
     * URL query parameter whose value is the type of report to generate.
     * The default is {@link ReportEnum#hierarchy}.
     * 
     * @see ReportEnum
     */
    public static final String REPORT = "report";
    
    /**
     * The ordered labels to be assigned to the category columns in a
     * {@link ReportEnum#pivot} report. The order of the names in the URL
     * query parameters MUST correspond with the order of the capturing
     * groups in the {@link #REGEX}.
     */
    public static final String CATEGORY = "category";
    
    /**
     * Name of the URL query parameter specifying whether the optional
     * correlated view for counter histories will be displayed.
     * <p>
     * Note: This is a shorthand for specifying {@link #REPORT} as
     * {@value ReportEnum#correlated}.
     */
    public static final String CORRELATED = "correlated";
    
    /**
     * Name of the URL query parameter specifying one or more strings for
     * the filter to be applied to the counter paths.
     */
    public static final String FILTER = "filter";

    /**
     * Name of the URL query parameter specifying one or more regular
     * expression for the filter to be applied to the counter paths. Any
     * capturing groups in this regular expression will be used to generate
     * the column title when examining correlated counters in a table view.
     * If there are no capturing groups then the counter name is used as the
     * default title.
     */
    public static final String REGEX = "regex";

    /**
     * Name of the URL query parameter specifying that the format for the first
     * column of the history counter table view. This column is the timestamp
     * associated with the counter but it can be reported in a variety of ways.
     * The possible values for this option are specified by
     * {@link TimestampFormatEnum}.
     * 
     * @see TimestampFormatEnum
     * 
     * @todo add support for elapsed period units since the fromTime, since a
     *       specified time, or since the federation up time.
     */
    public static final String TIMESTAMP_FORMAT = "timestampFormat";

    /**
     * The reporting period to be displayed. When not specified, all periods
     * will be reported. The value may be any {@link PeriodEnum}.
     */
    public static final String PERIOD = "period";

    /**
     * Optional override of the MIME type from a URL query parameter.
     */
    public static final String MIMETYPE = "mimeType";

    /**
     * Parameter recognized as the name of the local file on which to render the
     * counters (this option is supported only by utility classes run from a
     * command line, not by the httpd interface).
     */
    public static final String FILE = "file";
    
    /**
     * A collection of event filters. Each filter is a regular expression.
     * The key is the {@link Event} {@link Field} to which the filter will
     * be applied. The events filters are specified using URL query
     * parameters having the general form: <code>events.column=regex</code>.
     * For example,
     * 
     * <pre>
     * events.majorEventType = AsynchronousOverflow
     * </pre>
     * 
     * would select just the asynchronous overflow events and
     * 
     * <pre>
     * events.hostname=blade12.*
     * </pre>
     * 
     * would select events reported for blade12.
     */
    public final HashMap<Field,Pattern> eventFilters = new HashMap<Field, Pattern>();

    /**
     * The <code>eventOrderBy=fld</code> URL query parameters specifies
     * the sequence in which events should be grouped. The value of the
     * query parameter is an ordered list of the names of {@link Event}
     * {@link Field}s. For example:
     * 
     * <pre>
     * eventOrderBy=majorEventType &amp; eventOrderOrderBy=hostname
     * </pre>
     * 
     * would group the events first by the major event type and then by the
     * hostname. All events for the same {@link Event#majorEventType} and
     * the same {@link Event#hostname} would appear on the same Y value.
     * <p>
     * If no value is specified for this URL query parameter then the
     * default is as if {@link Event#hostname} was specified.
     */
    static final String EVENT_ORDER_BY = "eventOrderBy";
    
    /**
     * The order in which the events will be grouped.
     * 
     * @see #EVENT_ORDER_BY
     */
    public final Field[] eventOrderBy;
    
    /**
     * The URI from the request.
     */
    final public String uri;

    /**
     * The parameters from the request (as parsed from URL query parameters).
     */
    final public LinkedHashMap<String,Vector<String>> params;
    
//    /**
//     * The request headers.
//     */
//    final public Map<String,String> headers;
    
    /**
     * The reconstructed request URL.
     */
    private final String requestURL;
    
    /**
     * The value of the {@link #PATH} query parameter. 
     */
    final public String path;
    
    /**
     * The value of the {@link #DEPTH} query parameter.
     */
    final public int depth;
    
    /**
     * The kind of report to generate.
     * 
     * @see #REPORT
     * @see ReportEnum
     */
    final public ReportEnum reportType;
    
    /**
     * @see #TIMESTAMP_FORMAT
     * @see TimestampFormatEnum
     */
    final public TimestampFormatEnum timestampFormat;

    /**
     * The ordered labels to be assigned to the category columns in a
     * {@link ReportEnum#pivot} report (optional). The order of the names in
     * the URL query parameters MUST correspond with the order of the
     * capturing groups in the {@link #REGEX}.
     * 
     * @see #CATEGORY
     */
    final public String[] category;
    
    /**
     * The inclusive lower bound in milliseconds of the timestamp for the
     * counters or events to be selected.
     */
    final public long fromTime;

    /**
     * The exclusive upper bound in milliseconds of the timestamp for the
     * counters or events to be selected.
     */
    final public long toTime;
    
    /**
     * The reporting period to be used. When <code>null</code> all periods
     * will be reported. When specified, only that period is reported.
     */
    final public PeriodEnum period;
    
    /**
     * The {@link Pattern} compiled from the {@link #FILTER} query
     * parameters and <code>null</code> iff there are no {@link #FILTER}
     * query parameters.
     */
    final public Pattern pattern;

    /**
     * The events iff they are available from the service.
     * 
     * @see IEventReportingService
     */
    final public IEventReportingService eventReportingService;
    
    /**
     * <code>true</code> iff we need to output the scripts to support
     * <code>flot</code>.
     */
    final public boolean flot;
    
    /**
     * Used to format double and float counter values.
     */
    public final DecimalFormat decimalFormat;
    
    /**
     * Used to format counter values that can be inferred to be a percentage.
     */
    public final NumberFormat percentFormat;
    
    /**
     * Used to format integer and long counter values.
     */
    public final NumberFormat integerFormat;
    
    /**
     * Used to format the units of time when expressed as elapsed units since
     * the first sample of a {@link History}.
     */
    public final DecimalFormat unitsFormat;

    /**
     * Used to format the timestamp fields (From:, To:, and the last column) and
     * the epoch for <code>flot</code>. This is set dynamically based on the
     * {@link #TIMESTAMP_FORMAT} and the {@link #PERIOD}. Flot always requires
     * epoch numbering, so it does not use this field.
     */
    public final Format dateFormat;

    /**
     * Optional override of the MIME type from a URL query parameter.
     * 
     * @see MIMETYPE
     */
    public final String mimeType;
    
    /**
     * The name of a local file on which to write the data (this option is
     * supported only by local utility classes, not by the httpd interface).
     * 
     * @see #FILE
     */
    final public File file;
    
    @Override
    public String toString() {
        
        final StringBuilder sb = new StringBuilder();
        
        sb.append(URLQueryModel.class.getName());

        sb.append("{uri=" + uri);
        
        sb.append(",params=" + params);
        
        sb.append(",path=" + path);
        
        sb.append(",depth=" + depth);
        
        sb.append(",reportType=" + reportType);

        sb.append(",mimeType=" + mimeType);

        sb.append(",pattern=" + pattern);
        
        sb.append(",category="
                + (category == null ? "N/A" : Arrays.toString(category)));
        
        sb.append(",period=" + period);
        
        sb.append(",[fromTime=" + fromTime);
        
        sb.append(",toTime=" + toTime + "]");

        sb.append(",flot=" + flot);

        if (eventOrderBy != null) {
            sb.append(",eventOrderBy=[");
            boolean first = true;
            for (Field f : eventOrderBy) {
                if (!first)
                    sb.append(",");
                sb.append(f.getName());
                first = false;
            }
            sb.append("]");
        }

        if (eventFilters != null && !eventFilters.isEmpty()) {
            sb.append(",eventFilters{");
            boolean first = true;
            for (Map.Entry<Field, Pattern> e : eventFilters.entrySet()) {
                if (!first)
                    sb.append(",");
                sb.append(e.getKey().getName());
                sb.append("=");
                sb.append(e.getValue());
                first = false;
            }
            sb.append("}");
        }

        sb.append("}");

        return sb.toString();
        
    }
    
    /**
     * Factory for performance counter integration.
     * 
     * @param service
     *            The service object IFF one was specified when
     *            {@link CounterSetHTTPD} was started.
     * @param uri
     *            Percent-decoded URI without parameters, for example
     *            "/index.cgi"
     * @param parms
     *            Parsed, percent decoded parameters from URI and, in case of
     *            POST, data. The keys are the parameter names. Each value is a
     *            {@link Vector} of {@link String}s containing the bindings for
     *            the named parameter. The order of the URL parameters is
     *            preserved by the insertion order of the {@link LinkedHashMap}
     *            and the elements of the {@link Vector} values.
     * @param header
     *            Header entries, percent decoded
     */
    public static URLQueryModel getInstance(//
            final IService service,//
            final String uri,//
            final LinkedHashMap<String, Vector<String>> params,//
            final Map<String, String> headers//
            ) {

        /*
         * Re-create the request URL, including the protocol, host, port, and
         * path but not any query parameters.
         */

        final StringBuilder sb = new StringBuilder();

        // protocol (known from the container).
        sb.append("http://");

        // host and port
        sb.append(headers.get("host"));

        // path (including the leading '/')
        sb.append(uri);
        
        final String requestURL = sb.toString();

        return new URLQueryModel(service, uri, params, requestURL);

    }

    /**
     * Factory for Servlet API integration.
     * 
     * @param service
     *            The service object IFF one was specified when
     *            {@link CounterSetHTTPD} was started. If this implements the
     *            {@link IEventReportingService} interface, then events can also
     *            be requested.
     * 
     * @param req
     *            The request.
     * @param resp
     *            The response.
     */
    public static URLQueryModel getInstance(//
            final IService service,
            final HttpServletRequest req,
            final HttpServletResponse resp
            ) throws UnsupportedEncodingException {
        
        final String uri = URLDecoder.decode(req.getRequestURI(), "UTF-8");
        
        final LinkedHashMap<String, Vector<String>> params = new LinkedHashMap<String, Vector<String>>();

//        @SuppressWarnings("unchecked")
        final Enumeration<String> enames = req.getParameterNames();

        while (enames.hasMoreElements()) {

            final String name = enames.nextElement();

            final String[] values = req.getParameterValues(name);

            final Vector<String> value = new Vector<String>();

            for (String v : values) {

                value.add(v);

            }

            params.put(name, value);
            
        }
        
        final String requestURL = req.getRequestURL().toString();
        
        return new URLQueryModel(service, uri, params, requestURL);
        
    }
    
    /**
     * Create a {@link URLQueryModel} from a URL. This is useful when serving
     * historical performance counter data out of a file.
     * 
     * @param url
     *            The URL.
     * 
     * @return The {@link URLQueryModel}
     * 
     * @throws UnsupportedEncodingException
     */
    static public URLQueryModel getInstance(final URL url)
            throws UnsupportedEncodingException {

        // Extract the URL query parameters.
        final LinkedHashMap<String, Vector<String>> params = NanoHTTPD
                .decodeParams(url.getQuery(),
                        new LinkedHashMap<String, Vector<String>>());

        // add any relevant headers
        final Map<String, String> headers = new TreeMap<String, String>(
                new CaseInsensitiveStringComparator());

        headers.put("host", url.getHost() + ":" + url.getPort());

        return URLQueryModel.getInstance(null/* service */, url.toString(),
                params, headers);

    }

    private URLQueryModel(final IService service, final String uri,
            final LinkedHashMap<String, Vector<String>> params,
            final String requestURL) {

        if (uri == null)
            throw new IllegalArgumentException();

        if (params == null)
            throw new IllegalArgumentException();

        if (requestURL == null)
            throw new IllegalArgumentException();

        this.uri = uri;

        this.params = params;
        
//        this.headers = headers;

        this.requestURL = requestURL;
        
        this.path = getProperty(params, PATH, ICounterSet.pathSeparator);

        if (log.isInfoEnabled())
            log.info(PATH + "=" + path);

        this.depth = Integer.parseInt(getProperty(params, DEPTH, DEFAULT_DEPTH));
        
        if (log.isInfoEnabled())
            log.info(DEPTH + "=" + depth);
        
        if (depth < 0)
            throw new IllegalArgumentException("depth must be GTE ZERO(0)");

        /*
         * FIXME fromTime and toTime are not yet being parsed. They should
         * be interpreted so as to allow somewhat flexible specification and
         * should be applied to both performance counter views and event
         * views.
         */
        fromTime = 0L;
        toTime = Long.MAX_VALUE;

        // assemble the optional filter.
        this.pattern = QueryUtil.getPattern(//
                params.get(FILTER),//
                params.get(REGEX)//
                );

        if (service != null && service instanceof IEventReportingService) {

            // events are available.
            eventReportingService = ((IEventReportingService) service);

        } else {

            // events are not available.
            eventReportingService = null;
            
        }
        
        if (params.containsKey(REPORT) && params.containsKey(CORRELATED)) {

            throw new IllegalArgumentException("Please use either '"
                    + CORRELATED + "' or '" + REPORT + "'");
            
        }

        if(params.containsKey(REPORT)) {
        
            this.reportType = ReportEnum.valueOf(getProperty(
                params, REPORT, ReportEnum.hierarchy.toString()));

            if (log.isInfoEnabled())
                log.info(REPORT + "=" + reportType);
            
        } else {
        
            final boolean correlated = Boolean.parseBoolean(getProperty(
                    params, CORRELATED, "false"));
        
            if (log.isInfoEnabled())
                log.info(CORRELATED + "=" + correlated);

            this.reportType = correlated ? ReportEnum.correlated
                    : ReportEnum.hierarchy;

        }

        if (eventReportingService != null) {
            
            final Iterator<Map.Entry<String, Vector<String>>> itr = params
                    .entrySet().iterator();
            
            while(itr.hasNext()) {
                
                final Map.Entry<String, Vector<String>> entry = itr.next();
                
                final String name = entry.getKey();
                
                if (!name.startsWith("events."))
                    continue;
                
                final int pos = name.indexOf('.');

                if (pos == -1) {

                    throw new IllegalArgumentException(
                            "Missing event column name: " + name);
                    
                }
                
                // the name of the event column.
                final String col = name.substring(pos + 1, name.length());
                
                final Field fld;
                try {
                    
                    fld = Event.class.getField(col);
                    
                } catch(NoSuchFieldException ex) {

                    throw new IllegalArgumentException("Unknown event field: "+col);
                    
                }

                final Vector<String> patterns = entry.getValue();
                
                if (patterns.size() == 0)
                    continue;

                if (patterns.size() > 1)
                    throw new IllegalArgumentException(
                            "Only one pattern per field: " + name);
                
                /*
                 * compile the pattern
                 * 
                 * Note: Throws PatternSyntaxException if the pattern can
                 * not be compiled.
                 */
                final Pattern pattern = Pattern.compile(patterns.firstElement());
                
                eventFilters.put(fld, pattern);
                
            }

            if (log.isInfoEnabled()) {
                final StringBuilder sb = new StringBuilder();
                for (Field f : eventFilters.keySet()) {
                    sb.append(f.getName() + "=" + eventFilters.get(f));
                }
                log.info("eventFilters={" + sb + "}");
            }
            
        }
        
        // eventOrderBy
        {
            
            final Vector<String> v = params.get(EVENT_ORDER_BY);

            if (v == null) {

                /*
                 * Use a default for eventOrderBy.
                 */

                try {

                    eventOrderBy = new Field[] { Event.class
                            .getField("hostname") };

                } catch (Throwable t) {

                    throw new RuntimeException(t);

                }

            } else {

                final Vector<Field> fields = new Vector<Field>();

                for (String s : v) {

                    try {

                        fields.add(Event.class.getField(s));

                    } catch (Throwable t) {

                        throw new RuntimeException(t);

                    }

                }

                eventOrderBy = fields.toArray(new Field[0]);

            }
           
            if (log.isInfoEnabled())
                log.info(EVENT_ORDER_BY + "="
                        + Arrays.toString(eventOrderBy));

        }
        
        switch (reportType) {
        case events:
            if (eventReportingService == null) {

                /*
                 * Throw exception since the report type requires events but
                 * they are not available.
                 */

                throw new IllegalStateException("Events are not available.");

            }
            flot = true;
            break;
        default:
            flot = false;
            break;
        }
        
        this.category = params.containsKey(CATEGORY) ? params.get(CATEGORY)
                .toArray(new String[0]) : null;

        if (log.isInfoEnabled() && category != null)
            log.info(CATEGORY + "=" + Arrays.toString(category));

        this.timestampFormat = TimestampFormatEnum.valueOf(getProperty(
                params, TIMESTAMP_FORMAT, TimestampFormatEnum.dateTime.toString()));
        
        if (log.isInfoEnabled())
            log.info(TIMESTAMP_FORMAT + "=" + timestampFormat);

        this.period = PeriodEnum.valueOf(getProperty(params, PERIOD,
                PeriodEnum.Minutes.toString()/* defaultValue */));

        if (log.isInfoEnabled())
            log.info(PERIOD + "=" + period);

        /*
         * @todo this should be specified by a URL query parameter and
         * passed into the IRenderer instances.
         */
//        this.decimalFormat = new DecimalFormat("0.###E0");
        this.decimalFormat = new DecimalFormat("##0.#####E0");
        
//        decimalFormat.setGroupingUsed(true);
//
//        decimalFormat.setMinimumFractionDigits(3);
//        
//        decimalFormat.setMaximumFractionDigits(6);
//        
//        decimalFormat.setDecimalSeparatorAlwaysShown(true);
        
        this.percentFormat = NumberFormat.getPercentInstance();
        
        this.integerFormat = NumberFormat.getIntegerInstance();
        
        integerFormat.setGroupingUsed(true);
        
        this.unitsFormat = new DecimalFormat("0.#");
        
        /*
         * Figure out how we will format the timestamp (From:, To:, and the last
         * column).
         */
        switch(timestampFormat) {
        case dateTime:
            /*
             * Note: I have decided to go with the long format (date + time)
             * since runs often span days and the time along is not enough
             * information.
             */
          dateFormat = DateFormat.getDateTimeInstance(
                    DateFormat.MEDIUM/* date */, DateFormat.MEDIUM/* time */);
//            switch (period) {
//            case Minutes:
//                dateFormat = DateFormat.getTimeInstance(DateFormat.SHORT);
//                break;
//            case Hours:
//                dateFormat = DateFormat.getTimeInstance(DateFormat.MEDIUM);
//                break;
//            case Days:
//                dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM);
//                break;
//            default:
//                throw new UnsupportedOperationException(period.toString());
//            }
            break;
        case epoch: {
            // milliseconds since the epoch
            final NumberFormat f = NumberFormat.getIntegerInstance();
            f.setGroupingUsed(false);
            f.setMinimumFractionDigits(0);
            dateFormat = f;
            break;
        }
        default:
            throw new UnsupportedOperationException(timestampFormat.toString());
        }

        this.mimeType = (params.containsKey(MIMETYPE) ? getProperty(params,
                MIMETYPE, null) : null);

        this.file = (params.containsKey(FILE) ? new File(getProperty(params,
                FILE, null)) : null);

        if (log.isInfoEnabled())
            log.info(FILE + "=" + file);

    }

    /**
     * Return the first value for the named property.
     * 
     * @param params
     *            The request parameters.
     * @param property
     *            The name of the property
     * @param defaultValue
     *            The default value (optional).
     * 
     * @return The first value for the named property and the defaultValue
     *         if there named property was not present in the request.
     * 
     * @todo move to a request object?
     */
    static protected String getProperty(
            final Map<String, Vector<String>> params, final String property,
            final String defaultValue) {
        
        if (params == null)
            throw new IllegalArgumentException();

        if (property == null)
            throw new IllegalArgumentException();

        final Vector<String> vals = params.get(property);

        if (vals == null)
            return defaultValue;

        return vals.get(0);
        
    }

    /**
     * Re-create the request URL, including the protocol, host, port, and
     * path but not any query parameters.
     */
    public StringBuilder getRequestURL() {
        
        return new StringBuilder(requestURL);

    }

    /**
     * Re-create the request URL.
     * 
     * @param override
     *            Overridden query parameters (optional).
     *            
     * @todo move to request object?
     */
    public String getRequestURL(final URLQueryParam[] override) {
    
        // Note: Used throughput to preserve the parameter order.
        final LinkedHashMap<String,Vector<String>> p;
        
        if(override == null) {
            
            p = params;
            
        } else {
            
            p = new LinkedHashMap<String,Vector<String>>(params);
            
            for(URLQueryParam x : override) {
                                        
                p.put(x.name, x.values);
                
            }
            
        }

        final StringBuilder sb = getRequestURL();
        
        sb.append("?path="
                + encodeURL(getProperty(p, PATH, ICounterSet.pathSeparator)));
        
        final Iterator<Map.Entry<String, Vector<String>>> itr = p
                .entrySet().iterator();
        
        while(itr.hasNext()) {
            
            final Map.Entry<String, Vector<String>> entry = itr.next();

            final String name = entry.getKey();

            if (name.equals(PATH)) {

                // already handled.
                continue;

            }

            final Collection<String> vals = entry.getValue();

            for (String s : vals) {

                sb.append("&" + encodeURL(name) + "=" + encodeURL(s));

            }
            
        }
        
        return sb.toString();

    }

    static protected String encodeURL(final String url) {

        final String charset = "UTF-8";

        try {

            return URLEncoder.encode(url, charset);

        } catch (UnsupportedEncodingException e) {

            log.error("Could not encode: charset=" + charset + ", url="
                    + url);

            return url;

        }
        
    }
    
}

